﻿using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO;

namespace SmartCard.Framework.UnitTests
{
	[TestClass]
	public class ApplicationProtocolDataUnitTests
	{
		/// <summary>
		/// Verify that an APDU with an illigal command length can not
		/// be constructed
		/// </summary>
		[TestMethod]
		[ExpectedException(typeof(ArgumentException))]
		public void CommandTooLargeTest()
		{
			byte[] rawCommand = new byte[ushort.MaxValue + 1];
			var apdu = new ApduCommand(
				0x00, 0x00, 0x00, 0x00, rawCommand, 0);
		}

		/// <summary>
		/// Verify a simple apdu with no data can be created
		/// </summary>
		[TestMethod]
		public void NoCommandDataOrMaxResultLengthTest()
		{
			var expectedResult = BuildRawApdu(0x01, 0x02, 0x03, 0x04, null, null, null);

			ApduCommand apdu = new ApduCommand(
				0x01, 0x02, 0x03, 0x04, null, 0);

			var raw = apdu.GetRaw(false);
			AssertArrays(expectedResult, raw);
		}

		/// <summary>
		/// The message should only use one byte to specify
		/// command data length when the data length is 255 or
		/// less bytes.  This test also does not set an expected
		/// response length
		/// </summary>
		[TestMethod]
		public void CommandDataUnder256BytesLengthNoResponse()
		{
			byte[] rawCommandData = new byte[0xFF];

			var expectedResult = BuildRawApdu(
				0x01, 0x02, 0x03, 0x04, new byte[] { 0xFF },
				rawCommandData, null);

			ApduCommand apdu = new ApduCommand(
				0x01, 0x02, 0x03, 0x04, rawCommandData, 0);

			var raw = apdu.GetRaw(false);
			AssertArrays(expectedResult, raw);
		}

		[TestMethod]
		public void CommandDataGreaterThan256BytesLengthNoResponse()
		{
			byte[] rawCommandData = new byte[0xFF + 1];

			byte[] expectedResult = BuildRawApdu(
				0x01, 0x02, 0x03, 0x04,
				new byte[] { 0x00, 0x01, 0x00 },
				rawCommandData, null);

			ApduCommand apdu = new ApduCommand(
				0x01, 0x02, 0x03, 0x04, rawCommandData, 0);

			var raw = apdu.GetRaw(false);
			AssertArrays(expectedResult, raw);
		}

		/// <summary>
		/// The APDU states that if either the command lengh or the data length
		/// are greater than 0xFF then both values must be represented using the
		/// expanded 3 byte length identifier.
		/// </summary>
		[TestMethod]
		public void CommandDataGreaterThan256BytesLengthSmallResponse()
		{
			byte[] rawCommandData = new byte[0xFF + 1];

			byte[] expectedResult = BuildRawApdu(
					0x01, 0x02, 0x03, 0x04,
					new byte[] { 0x00, 0x01, 0x00 },
					rawCommandData,
					new byte[] { 0x00, 0x00, 0x01 });

			ApduCommand apdu = new ApduCommand(
				0x01, 0x02, 0x03, 0x04, rawCommandData, 1);

			var raw = apdu.GetRaw(false);
			AssertArrays(expectedResult, raw);
		}

		/// <summary>
		/// Test the APDU raw data structure when there is no command data specified,
		/// but a large response over 255 bytes in length specified
		/// </summary>
		[TestMethod]
		public void NoCommandDataLargeResponse()
		{
			byte[] expectedResult = BuildRawApdu(
					0x01, 0x02, 0x03, 0x04,
					null, null,
					new byte[] { 0x00, 0x01, 0x00 });

			ApduCommand apdu = new ApduCommand(
				0x01, 0x02, 0x03, 0x04, null, (0xFF + 1));

			var raw = apdu.GetRaw(false);
			AssertArrays(expectedResult, raw);
		}

		#region private test helper

		/// <summary>
		/// Helper method to consolodate code for building up a raw apdu message that will
		/// be used for validation during tests.  No validation is performed against the parameters
		/// this is just here to help consolodate some code
		/// </summary>
		/// <param name="instructionClass"></param>
		/// <param name="instructionCode"></param>
		/// <param name="param1"></param>
		/// <param name="param2"></param>
		/// <param name="commandDataLengthBytes"></param>
		/// <param name="commandData"></param>
		/// <param name="maxResponseLengthBytes"></param>
		/// <returns></returns>
		private static byte[] BuildRawApdu(byte instructionClass,
			byte instructionCode, byte param1, byte param2, byte[] commandDataLengthBytes,
			byte[] commandData, byte[] maxResponseLengthBytes)
		{
			byte[] raw = null;
			using (var ms = new MemoryStream())
			{
				ms.WriteByte(instructionClass);
				ms.WriteByte(instructionCode);
				ms.WriteByte(param1);
				ms.WriteByte(param2);

				if (commandDataLengthBytes != null)
				{ ms.Write(commandDataLengthBytes, 0, commandDataLengthBytes.Length); }

				if (commandData != null)
				{ ms.Write(commandData, 0, commandData.Length); }

				if (maxResponseLengthBytes != null)
				{ ms.Write(maxResponseLengthBytes, 0, maxResponseLengthBytes.Length); }

				raw = ms.ToArray();
			}

			return raw;
		}

		private static void AssertArrays<T>(T[] expected, T[] actual)
		{
			// if they reference the same item return
			if (expected == actual)
			{ return; }

			Assert.IsNotNull(actual);
			Assert.AreEqual(expected.Length, actual.Length);
			for (int i = 0; i != expected.Length; ++i)
			{
				Assert.AreEqual(expected[i], actual[i]);
			}
		}
		#endregion
	}
}
